import java.util.*;
import java.net.*;
import java.io.*;
import java.math.*;

public class User extends Thread{

    int input;
    int output;
    int ID;
    Player p;
    int totalBytesSend;
    HashMap<Integer, Integer> users;
    HashMap<Integer, Player> players;

    //network variables
    ServerSocket ss;
    Socket connection;
    ArrayList<Socket> sockets;
    OutputStream baseStream;
    DataOutputStream dos;
    ObjectOutputStream out;
    HashMap<Integer, BigInteger> compPhaseReconsPriv;


    //Random input generated
    public User(int ID, Player p, HashMap<Integer, Player> players){
        this.ID = ID;
        this.p = p;
        this.players = players;
        compPhaseReconsPriv = new HashMap<Integer, BigInteger>();
        Random rand = new Random(0);
        input = rand.nextInt(Utilities.F);
        startServer();
    }

    public User(int ID, int input, Player p, HashMap<Integer, Player> players){
        this.ID = ID;
        this.p = p;
        this.players = players;
        compPhaseReconsPriv = new HashMap<Integer, BigInteger>();
        if(input < 0 || input > Utilities.F){
            System.out.println("INPUT NOT AN INTEGER BETWEEN 0 AND |F| - you input " + input);
            System.exit(-1);
        }
        this.input = input;
        startServer();
    }

    public void init(HashMap<Integer, Integer> users){
        this.users = users;
    }

    private void startServer(){
        try{
            ss = new ServerSocket(2000+Utilities.n+ID);
            sockets = new ArrayList<Socket>(); //unbound sockets
            for(int i = 0; i<=Utilities.n; i++){
                sockets.add(new Socket());
            }
        }
        catch(IOException e){
            e.printStackTrace();
        }
    }

    public int getInput(){
        return input;
    }

    public void run(){       
        try{
            if(ss != null){
                connection = ss.accept();
                Utilities.lock.lock();
                ObjectInputStream in = new ObjectInputStream(connection.getInputStream());
                int code = (Integer)in.readObject();
                int sender = (Integer)in.readObject();
                BigInteger share1, share2;
                int caller;
                switch(code){  
                case Utilities.compPhaseReconsPriv:
                    share1 = (BigInteger)in.readObject();
                    caller = (Integer)in.readObject();            
                    compPhaseReconsPriv(share1, sender, caller);
                    break;
                    
                case Utilities.compPhaseReconsPrivFinal:
                    share1 = (BigInteger)in.readObject();
                    compPhaseReconsPrivFinal(share1, sender);
                    break;                    
                }                 
                Utilities.lock.unlock();
                run();
            }
        }
        catch(IOException e){
            e.printStackTrace();
        }
        catch(ClassNotFoundException e){
            e.printStackTrace();
        }
    }

    public void sendMessage(int code, ArrayList<Object> msg, int userID){
        Utilities.lock.lock();
        Socket s = sockets.remove(userID);
        try{
            s.connect(new InetSocketAddress("localhost", 2000+Utilities.n+userID));
            
            baseStream = s.getOutputStream();
                
            dos = new DataOutputStream(baseStream);
            out = new ObjectOutputStream(dos);
            out.writeObject(code);
            out.writeObject(ID); //always tag along yourself.
            for(Object o : msg){
                out.writeObject(o);
            }
            out.flush();
            totalBytesSend += dos.size();
        }
        catch(IOException e){
            e.printStackTrace();
        }
        finally{
            try{
                dos.close();
                out.close();
                s.close();
                //replace the old socket with a new one. 
                sockets.add(userID, new Socket());
            }
            catch(IOException e){
                e.printStackTrace();
            }
            finally{
                Utilities.lock.unlock();
            }
        }
    }

    public int totalBytesSend(){
        return totalBytesSend;
    }

    public void reconsPriv(int sendToID, int caller, Gate g){
        //Only invoke the reconstruction if p is still in the
        //game. o/w we will try getting a triple which is null. This
        //goes for both sending and being in charge of the eliminated
        Utilities.lock.lock();
        if(players.get(ID) == null){
            //I am the user responisble for a kicked player, thus I just return.
            if(caller == Utilities.OUTPUTGATE)
                p.circuit.addOutputGate((OutputGate)g); //in order to get an output
            Utilities.lock.unlock();
            return;        
        }
        if(caller == Utilities.OUTPUTGATE){
            p.circuit.addOutputGate((OutputGate)g);
            Gate resultGate = p.circuit.getGate(((OutputGate)g).gateType, ((OutputGate)g).gateID);
            BigInteger share = resultGate.getShare();
            
            ArrayList<Object> list = new ArrayList<Object>();
            list.add(share);
            list.add(caller);
            sendMessage(Utilities.compPhaseReconsPriv, list, sendToID);
            //p.cc.continueOutputGates(ID);
        }
        else{
            p.circuit.addInputGate((InputGate)g);
        
            //BigInteger share = p.triples.get(0)[0];
            BigInteger share = players.get(p.getOldID()).triples.get(0)[0];
            ArrayList<Object> list = new ArrayList<Object>();
            list.add(share);
            list.add(caller);
            sendMessage(Utilities.compPhaseReconsPriv, list, sendToID);
        }
        Utilities.lock.unlock();
    }

    /**
     * Receive shares from all users. Once we have all, we
     * reconstruct and broadcast d=s-r
     */
    public void compPhaseReconsPriv(BigInteger share, int senderID, int caller){ 
        if(compPhaseReconsPriv.containsKey(senderID)){
            System.err.println(">>>>> Error! - Player " + senderID 
                               + " sent a share for reconsPriv using the code: " + caller );
            p.error(p.RECONSPRIV);
            return;
        }
        compPhaseReconsPriv.put(senderID, share);
        if(compPhaseReconsPriv.size() == Utilities.n_prime){
            //Got all shares - now do the reconstruction
            BigInteger secret = p.sharingProtocol.reconsPriv(Utilities.t, compPhaseReconsPriv);
            //clear hashmap for next time
            compPhaseReconsPriv.clear();
            if(caller == Utilities.INPUTGATE){               
                //compute d=s-r , where r is the secret and s is the input. 
                BigInteger d = BigInteger.valueOf(input).subtract(secret).mod(Utilities.F_big);
                
                //broadcast d:
                for(Integer ID: players.keySet()){
                    ArrayList<Object> list = new ArrayList<Object>();
                    list.add(d);
                    sendMessage(Utilities.compPhaseReconsPrivFinal, list, ID);
                }
            }
            else{
                //outputgate
                //simply set the output gates share of my outputGate:
                p.circuit.initOutputGate(secret, ID);
                p.cc.continueOutputGates(ID);
            }
        }
    }

    /**
     * Receive the broadcasted share for the given gate. Now assign
     * s_i=d + r_i to the given input-gate (senderID).
     */
    public void compPhaseReconsPrivFinal(BigInteger d, int senderID){
        //Assign s_i = d+r_i , where r_i is the share used to
        //reconstruct earlier. and d = secret - r

        //if triples size is 0, then we have already told cc in reconsPriv
//         if(p.triples.size() == 0){
//             return;
//         }
        //We need to remove the triple and assign the input to the
        //correct input-gate
        BigInteger r_i = p.triples.remove(0)[0];
        BigInteger s_i = d.add(r_i).mod(Utilities.F_big);

        p.circuit.initInputGate(s_i, senderID);
        p.cc.continueInputGates(ID);
    }

}